package io.gitee.zhangbinhub.acp.cloud.resource.server.component

import io.gitee.zhangbinhub.acp.cloud.resource.server.tools.TokenTools
import io.gitee.zhangbinhub.acp.core.common.log.LogFactory
import org.springframework.core.ParameterizedTypeReference
import org.springframework.core.convert.converter.Converter
import org.springframework.http.*
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames
import org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException
import org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException
import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.client.RestOperations
import java.net.URI

class AcpOpaqueTokenIntrospect(
    introspectionUri: String,
    private val restOperations: RestOperations
) : OpaqueTokenIntrospector {
    private val logger = LogFactory.getInstance(this.javaClass)
    private val stringObjectMap: ParameterizedTypeReference<Map<String, Any?>?> =
        object : ParameterizedTypeReference<Map<String, Any?>?>() {}
    private val requestEntityConverter: Converter<String?, RequestEntity<*>> =
        defaultRequestEntityConverter(URI.create(introspectionUri))

    @Throws(OAuth2IntrospectionException::class)
    override fun introspect(token: String?): OAuth2AuthenticatedPrincipal {
        if (token == null) {
            throw OAuth2IntrospectionException("token is null")
        }
        val requestEntity = requestEntityConverter.convert(token)
        return if (requestEntity == null) {
            throw OAuth2IntrospectionException("requestEntityConverter returned a null entity")
        } else {
            val responseEntity = makeRequest(requestEntity)
            val claims = adaptToNimbusResponse(responseEntity)
            TokenTools.convertClaimsSet(claims.toMutableMap())
        }
    }

    private fun requestHeaders(): HttpHeaders {
        val headers = HttpHeaders()
        headers.accept = listOf(MediaType.APPLICATION_JSON)
        return headers
    }

    private fun requestBody(token: String?): MultiValueMap<String, String?> {
        val body: MultiValueMap<String, String?> = LinkedMultiValueMap()
        body.add("token", token)
        return body
    }

    private fun defaultRequestEntityConverter(introspectionUri: URI): Converter<String?, RequestEntity<*>> =
        Converter { token: String? ->
            RequestEntity(
                requestBody(token),
                requestHeaders(),
                HttpMethod.POST,
                introspectionUri
            )
        }

    @Throws(OAuth2IntrospectionException::class, BadOpaqueTokenException::class)
    private fun makeRequest(requestEntity: RequestEntity<*>): ResponseEntity<Map<String, Any?>?> = try {
        restOperations.exchange(requestEntity, stringObjectMap)
    } catch (ex: HttpClientErrorException) {
        throw BadOpaqueTokenException(ex.responseBodyAsString, ex)
    } catch (ex: Exception) {
        throw OAuth2IntrospectionException(ex.message, ex)
    }

    @Throws(OAuth2IntrospectionException::class, BadOpaqueTokenException::class)
    private fun adaptToNimbusResponse(responseEntity: ResponseEntity<Map<String, Any?>?>): Map<String, Any?> {
        if (responseEntity.statusCode != HttpStatus.OK) {
            throw OAuth2IntrospectionException(
                "Introspection endpoint responded with " + responseEntity.statusCode
            )
        }
        val claims = (responseEntity.body ?: mapOf()).toMutableMap()
        val active = claims.compute(OAuth2TokenIntrospectionClaimNames.ACTIVE) { _: String, value: Any? ->
            if (value is String) {
                return@compute value.toBoolean()
            }
            if (value is Boolean) {
                return@compute value
            }
            false
        } as Boolean
        if (!active) {
            logger.trace("Did not validate token since it is inactive")
            throw BadOpaqueTokenException("Provided token isn't active")
        }
        return claims
    }
}